﻿<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
	<title>Involute Spur Gear Builder</title>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
	<meta name="description" content="Involute spur gear builder with DXF output. Licensed under the MIT license (http://opensource.org/licenses/mit-license.php). Copyright 2013 Dr. Rainer Hessmer">
	<meta name="author" content="Dr. Rainer Hessmer">

	<script type="text/javascript" src="OpenJsCad/lightgl.js" ></script>
	<script type="text/javascript" src="OpenJsCad/csg.js"></script>
	<script type="text/javascript" src="OpenJsCad/openjscad.js"></script>
	<style type="text/css">

		body {
			font: 14px/20px 'Helvetica Neue Light', HelveticaNeue-Light, 'Helvetica Neue', Helvetica, Arial, sans-serif;
			max-width: 820px;
			margin: 0 auto;
			padding: 10px;
		}

		pre, code, textarea {
			font: 12px/20px Monaco, monospace;
			border: 1px solid #CCC;
			border-radius: 3px;
			background: #F9F9F9;
			padding: 0 3px;
			color: #555;
		}
		pre, textarea {
			padding: 10px;
			width: 100%;
		}
		textarea {
			height: 200px;
		}
		textarea:focus {
			outline: none;
		}

		canvas { cursor: move; }

	</style>
	<link rel="stylesheet" href="OpenJsCad/openjscad.css" type="text/css">

	<script type="text/javascript">
		var gProcessor=null;

		// Show all exceptions to the user:
		OpenJsCad.AlertUserOfUncaughtExceptions();

		function onload()
		{
		  var viewerOptions = {
			drawAxes: false,
			rotateZX: false,
			rotateXY: false
		  }
		  gProcessor = new OpenJsCad.Processor(document.getElementById("viewer"), viewerOptions);
		  updateSolid();
		}

		function updateSolid()
		{
		  var jscadCode = document.getElementById('scriptContainer').getElementsByTagName("script")[0].text;
		  gProcessor.setJsCad(jscadCode, "CycloidalGear");
		}
	</script>
</head>

<body onload="onload()">
	<h1>Involute Spur Gear Builder <span style="font-size:10px">(C) 2013 Dr. Rainer Hessmer</span></h1>
	<p>An open source, browser based utility for calculating and drawing involute spur gears. As an improvement over the majority of other freely available scripts and utilities it fully accounts for undercuts. For additional information please head over to my <a href="http://www.hessmer.org/blog/2014/01/01/online-involute-spur-gear-builder">associated blog post</a>.</p>
	<p>The implementation is inspired by the subtractive process that Michal Zalewski's describes in <a href="http://lcamtuf.coredump.cx/gcnc/ch6/">part six</a> of his excellent <a href=" http://lcamtuf.coredump.cx/gcnc/">Guerrilla guide to CNC machining, mold making, and resin casting</a>.</p>
    <h2>Instructions</h2>
    <p>Specify desired values in the parameters box and then click on the 'Update' button. Dependent on the specified quality level the rendering might take some time. Once done, the result can be saved as a DXF file by clicking the 'Generate DXF' button underneath the graphics window.</p>
    <p>Use the mouse scroll wheel or the slider underneath the window to zoom in and out. Press the left mouse button and move the mouse to pan.</p>
	<div id="viewer"></div>

	<div id="scriptContainer">
		<script type="text/javascript">
			// this is the jscad script
			var g_ExpandToCAGParams = {pathradius: 0.2, resolution: 2};

			function main(params)
			{
				// Main entry point; here we construct our solid:
				var qualitySettings = initializeQualitySettings(params.qualityOption)

				var gear1 = new Gear({
					circularPitch: params.circularPitch,
					pressureAngle: params.pressureAngle,
					clearance: params.clearance,
					backlash: params.backlash,
					toothCount: params.wheel1ToothCount,
					centerHoleDiameter: params.wheel1CenterHoleDiamater,
					circularPitch: params.circularPitch,
					qualitySettings: qualitySettings
				});
				var gear2 = new Gear({
					circularPitch: params.circularPitch,
					pressureAngle: params.pressureAngle,
					clearance: params.clearance,
					backlash: params.backlash,
					toothCount: params.wheel2ToothCount,
					centerHoleDiameter: params.wheel2CenterHoleDiamater,
					circularPitch: params.circularPitch,
					qualitySettings: qualitySettings
				});
						
				var gearSet = new GearSet(
					gear1,
					gear2,
					params.showOption);

				var shape = gearSet.createShape();
				return shape;
			}
			
			function initializeQualitySettings(qualityOption) {
				// default values (draft quality)
				var resolution = 60;  // Number of segments used per 360 degrees when drawing curves
				var stepsPerToothAngle = 3; // determines the angular step size when assembling the tooth profile
				
				if (qualityOption == 1) {
					// normal quality
					resolution = 180;
					stepsPerToothAngle = 10;
				}
				else if (qualityOption == 2) {
					// high quality
					resolution = 360;
					stepsPerToothAngle = 20;
				}
				return {resolution: resolution, stepsPerToothAngle: stepsPerToothAngle};
			}

			function getParameterDefinitions() {
				return [
					{ name: 'circularPitch', caption: 'Circular pitch (distance from one face of a tooth to the corresponding face of an adjacent tooth on the same gear, measured along the pitch circle):', type: 'float', initial: 50 },
					{ name: 'pressureAngle', caption: 'Pressure Angle (common values are 14.5, 20 and 25 degrees):', type: 'float', initial: 20 },
					{ name: 'clearance', caption: 'Clearance (minimal distance between the apex of a tooth and the trough of the other gear; in length units):', type: 'float', initial: 0.05 },
					{ name: 'backlash', caption: 'Backlash (minimal distance between meshing gears; in length units):', type: 'float', initial: 0.05 },
					{ name: 'wheel1ToothCount', caption: 'Wheel 1 Tooth Count:', type: 'int', initial: 8 },
					{ name: 'wheel1CenterHoleDiamater', caption: 'Wheel 1 Center Hole Diameter (0 for no hole):', type: 'float', initial: 4 },
					{ name: 'wheel2ToothCount', caption: 'Wheel 2 Tooth Count:', type: 'int', initial: 8 },
					{ name: 'wheel2CenterHoleDiamater', caption: 'Wheel 2 Center Hole Diameter (0 for no hole):', type: 'float', initial: 4 },
					{ name: 'showOption', caption: 'Show:', type: 'choice', values: [3, 1, 2], initial: 3, captions: ["Wheel 1 and Wheel 2", "Wheel 1 Only", "Wheel 2 Only"]},
					//{ name: 'resolution', caption: 'Resolution (number of segments per 360 degree of rotation, set to 200 or higher for production quality):', type: 'int', initial: 40 },
					{ name: 'qualityOption', caption: 'Quality level (better means longer waits):', type: 'choice', values: [0, 1, 2], initial: 0, captions: ["Draft", "Normal", "High"]},
				];
			}

			// Generic support for 'class' inheritance (see TypeScript playground example 'Simple Inheritance' for details (http://www.typescriptlang.org/Playground/)
			var __extends = this.__extends || function (d, b) {
				function __() { this.constructor = d; }
				__.prototype = b.prototype;
				d.prototype = new __();
			};
			// End generic support for 'class' inheritance

			// Start base class Gear
			var Gear = (function () {
				function Gear(options) {
					var options = options || {};

					this.toothCount = options.toothCount || 15;
					this.circularPitch = options.circularPitch;    // Distance from one face of a tooth to the corresponding face of an adjacent tooth on the same gear, measured along the pitch circle.
					this.diametralPitch = options.diametralPitch;  // Ratio of the number of teeth to the pitch diameter
					this.pressureAngle = options.pressureAngle || 20; // Most common stock gears have a 20° pressure angle, with 14½° and 25° pressure angle gears being much less
					// common. Increasing the pressure angle increases the width of the base of the gear tooth, leading to greater strength and load carrying capacity. Decreasing
					// the pressure angle provides lower backlash, smoother operation and less sensitivity to manufacturing errors. (reference: http://en.wikipedia.org/wiki/Involute_gear)

					this.clearance = options.clearance || 0;
					this.centerHoleDiameter = options.centerHoleDiameter || 0;

					// Given either circular pitch or diametral pitch we calculate the other value
					if (this.circularPitch) {
						// convert circular pitch to diametral pitch
						this.diametralPitch = Math.PI / this.circularPitch;
					}
					else if (this.circularPitch) {
						// convert diametral pitch to circular pitch
						this.circularPitch = Math.PI / this.diametralPitch;
					}
					else {
						throw "gear module needs either a diametralPitch or circularPitch";
					}
					
					this.backlash = options.backlash || 0;
					if (this.backlash < 0) {
						this.backlash = 0;
					}

					this.center = [0,0]; // center of the gear
					this.angle = 0; // angle in degrees of the complete gear (changes during rotation animation)
					

					// Pitch diameter: Diameter of pitch circle.
					this.pitchDiameter = this.toothCount / this.diametralPitch;
					this.pitchRadius = this.pitchDiameter / 2;

					// Addendum: Radial distance from pitch circle to outside circle.
					this.addendum = 1 / this.diametralPitch;

					//Outer Circle
					this.outerRadius = this.pitchRadius + this.addendum;
					this.angleToothToTooth = 360 / this.toothCount;
					this.qualitySettings = options.qualitySettings;
					//OpenJsCad.log("qualitySettings.resolution: " + this.qualitySettings.resolution);
					//OpenJsCad.log("qualitySettings.stepsPerToothAngle: " + this.qualitySettings.stepsPerToothAngle);
				}
				Gear.prototype.createShape = function() {
					// create outer circle sector covering half a tooth
					var halfToothSectorPath = new CSG.Path2D([[0,0]], /* closed = */ false);
					var halfToothSectorArc = CSG.Path2D.arc({
						center: [0, 0],
						radius: this.outerRadius,
						startangle: 90,
						endangle: 90 - this.angleToothToTooth / 2,
						resolution: this.qualitySettings.resolution,
					});
					halfToothSectorPath = halfToothSectorPath.concat(halfToothSectorArc);
					halfToothSectorPath = halfToothSectorPath.close();
					var halfToothSector = halfToothSectorPath.innerToCAG();
					
					var gearShape = new CAG();
					gearShape = gearShape.union(halfToothSector);

					var halfToothCutout = this.createHalfToothCutout(true);
					var halfTooth = halfToothSector.subtract(halfToothCutout);
					var tooth = halfTooth.union(halfTooth.mirroredX());

					var gearShape = new CAG();
					
					for(var i = 0; i < this.toothCount; i++) {
						var angle = i * this.angleToothToTooth;
						var rotatedtooth = tooth.rotateZ(angle);
						gearShape = gearShape.union(rotatedtooth);
					}

					// if (this.centerHoleDiameter > 0) {
						// var centerhole = CAG.circle({center: [-0, -0], radius: this.centerHoleDiameter / 2, resolution: this.qualitySettings.resolution});
						// gearShape = gearShape.subtract(centerhole);
					// }
					
					// apply gear rotation
					//OpenJsCad.log(this.angle);
					gearShape = gearShape.rotateZ(this.angle + 90)
					// move to correct center
					gearShape = gearShape.translate(this.center);
					return gearShape;
				}
				Gear.prototype.createCutoutDemo = function() {
					// create outer circle
					var outerCirclePath = CSG.Path2D.arc({
						center: [0, 0],
						radius: this.outerRadius,
						startangle: 0,
						endangle: 360,
						resolution: this.qualitySettings.resolution,
					});
					outerCirclePath = outerCirclePath.close();

					var gearShape = new CAG();
					gearShape = gearShape.union(outerCirclePath.expandToCAG(g_ExpandToCAGParams.pathradius, g_ExpandToCAGParams.resolution));
					
					var firstCutoutHalf = this.createHalfToothCutout(true);
					firstCutoutHalf = firstCutoutHalf.rotateZ(180 / this.toothCount);
					// for illustration purposes we mirror the cutout and rotate it so that we can see a completely formed tooth 
					//var secondCutoutHalf = firstCutoutHalf.rotateZ(360 / this.toothCount);
					var secondCutoutHalf = firstCutoutHalf.mirroredX();

					gearShape = gearShape.union(firstCutoutHalf);
					gearShape = gearShape.union(secondCutoutHalf);
					
					//var toothCutter = this.createHalfToothCutout(true);
					//gearShape = gearShape.union(toothCutter);
					
					// apply gear rotation
					gearShape = gearShape.rotateZ(this.angle + 90)
					// move to correct center
					gearShape = gearShape.translate([this.pitchRadius, 0]);
					return gearShape;
				}
				Gear.prototype.createHalfToothCutout = function(asPath) {
					var angleToothToTooth = 360 / this.toothCount;
					var angleStepSize = 10; //this.angleToothToTooth / this.qualitySettings.stepsPerToothAngle;
					//OpenJsCad.log("angleToothToTooth: " + this.angleToothToTooth);
					//OpenJsCad.log("angleStepSize: " + angleStepSize);
					var toothCutout = new CAG();
					
					var toothCutter = this.createToothCutter(asPath);
					var toothCutterShape = toothCutter.shape;
					var lowerLeftCorner = toothCutter.lowerLeftCorner;
					
					// To create the tooth profile we move the (virtual) infinite gear and then turn the resulting cutter position back. 
					// For illustration see http://lcamtuf.coredump.cx/gcnc/ch6/, section 'Putting it all together'
					// We continue until the moved tooth cutter's lower left corner is outside of the outer circle of the gear.
					// Going any further will no longer influence the shape of the tooth
					var lowerLeftCornerDistance = 0;
					var stepCounter = 0;
					while (true) {
						var angle = stepCounter * angleStepSize;
						var xTranslation = [angle * Math.PI / 180 * this.pitchRadius, 0];

						var movedLowerLeftCorner = lowerLeftCorner.translate(xTranslation);
						movedLowerLeftCorner = movedLowerLeftCorner.rotateZ(angle);
						
						lowerLeftCornerDistance = movedLowerLeftCorner.length();
						if (movedLowerLeftCorner.length() > this.outerRadius) {
							// the cutter is now completely outside the gear and no longer influences the shape of the gear tooth
							break;
						}

						// we move in both directions
						var movedToothCutterShape = toothCutterShape.translate(xTranslation);
						var movedToothCutterShape = movedToothCutterShape.rotateZ(angle);
						toothCutout = toothCutout.union(movedToothCutterShape);
	
						if (xTranslation[0] > 0) {
							//OpenJsCad.log("xTranslation: " + xTranslation);
							movedToothCutterShape = toothCutterShape.translate([-xTranslation[0], xTranslation[1]]);
							movedToothCutterShape = movedToothCutterShape.rotateZ(-angle);
							toothCutout = toothCutout.union(movedToothCutterShape);
						}
						
						stepCounter++;
					}
					
					//toothCutout = this.smoothen(toothCutout);
					//var outlinePaths = toothCutout.getOutlinePaths();

					//OpenJsCad.log(this.pointsToString(outlinePaths[0].points));
					//OpenJsCad.log(toothCutout.isSelfIntersecting());
					
					
					//return toothCutout.rotateZ(-this.angleToothToTooth / 2);
					return toothCutout;
				}
				Gear.prototype.createToothCutterRack = function(asPath) {
					var toothWidth = this.circularPitch / 2;
					var lowerLeftCorner = [-10 * toothWidth, this.pitchRadius + this.addendum];
					var upperLeftCorner = [-10 * toothWidth, this.pitchRadius + 2 * this.addendum];
					var lowerRightCorner = [10 * toothWidth, this.pitchRadius + this.addendum];
					var upperRightCorner = [10 * toothWidth, this.pitchRadius + 2 * this.addendum];
					var rect = new CSG.Path2D(
						[lowerLeftCorner, upperLeftCorner, upperRightCorner, lowerRightCorner],
						/* closed = */ true
					);
					
					var cutterShape = rect.innerToCAG();
					var singleCutterTooth = this.createToothCutterSingle(false).shape;
					
					for(var i = -3; i < 4; i++) {
						var moved = singleCutterTooth.translate([i * this.circularPitch, 0]);
						cutterShape = cutterShape.union(moved);
					}
					return cutterShape;
				}
				Gear.prototype.createToothCutter = function(asPath) {
					// we create a trapezoidal cutter as described at http://lcamtuf.coredump.cx/gcnc/ch6/ under the section 'Putting it all together'
					var toothWidth = this.circularPitch / 2;
					//OpenJsCad.log("toothWidth: " + toothWidth);
					OpenJsCad.log("addendum: " + this.addendum);
					OpenJsCad.log("clearance: " + this.clearance);
					
					var cutterDepth = this.addendum + this.clearance;
					var cutterOutsideLength = 2 * this.addendum;
					OpenJsCad.log("cutterDepth: " + cutterDepth);
					//OpenJsCad.log("cutterOutsideLength: " + cutterOutsideLength);
					
					var sinPressureAngle = Math.sin(this.pressureAngle * Math.PI / 180);
					var cosPressureAngle = Math.cos(this.pressureAngle * Math.PI / 180);

					// if a positive backlash is defined then we widen the trapezoid accordingly.
					// Each side of the tooth needs to widened by a fourth of the backlash (vertical to cutter faces).
					var dx = this.backlash / 4 / cosPressureAngle;
					//OpenJsCad.log("backlash: " + this.backlash);
					//OpenJsCad.log("dx: " + dx);
					
					var lowerRightCorner = [toothWidth / 2 + dx - cutterDepth * sinPressureAngle, this.pitchRadius - cutterDepth];
					var upperRightCorner = [toothWidth / 2 + dx  + cutterOutsideLength * sinPressureAngle, this.pitchRadius + cutterOutsideLength];
					var upperLeftCorner = [-upperRightCorner[0], upperRightCorner[1]];
					var lowerLeftCorner = [-lowerRightCorner[0], lowerRightCorner[1]];
					
					//this.logPoints([lowerRightCorner, upperRightCorner, upperLeftCorner, lowerLeftCorner]);
					
					var cutterPath = new CSG.Path2D(
						[lowerLeftCorner, upperLeftCorner, upperRightCorner, lowerRightCorner],
						/* closed = */ true
					);
					
					var cutterShape;
					if (asPath) {
						cutterShape = cutterPath.expandToCAG(g_ExpandToCAGParams.pathradius, g_ExpandToCAGParams.resolution);
					}
					else {
						cutterShape = cutterPath.innerToCAG();
					}
					return {
						shape: cutterShape,
						lowerLeftCorner: cutterPath.points[0] 
					}
				}
				Gear.prototype.pointsToString = function(points) {
					var result = "[";
					points.map(function(point) {
						result += "[" + point.x + "," + point.y + "],";
					});
					return result + "]";
				}
				return Gear;
			})();

			// GearSet class
			var GearSet = (function () {
				function GearSet(gear1, gear2, showOption) {
					this.gear1 = gear1;
					gear1.connectedGear = gear2;
					this.gear2 = gear2;
					gear2.connectedGear = gear1;
					// in order for the two gears to mesh we need to turn the second one by 'half a tooth'
					//this.gear1.setAngle(0);
					this.gearRatio = this.gear1.toothCount / this.gear1.toothCount;
					this.gearsDistance = this.gear1.pitchRadius + this.gear2.pitchRadius;	
					this.gear2.center = [0, this.gearsDistance];
					
					// we need an angle offset of half a tooth for the two gears to mesh
					this.gear2.angle = 180 + 180 / this.gear2.toothCount;

					this.showOption = showOption;
				}
				GearSet.prototype.createShape = function() {
					var shape = new CAG();
					if ((this.showOption & 1) > 0) {
						// show gear 1
						var gear1Shape = this.gear1.createShape();
						//var gear1Shape = this.gear1.createCutoutDemo();
						shape = shape.union(gear1Shape);
					}
					return shape;
				}

				return GearSet;
			})();

		</script>
	</div>

    <h2>License and Credits</h2>

    <ul>
        <li>The application itself, as well as the application specific source code is copyright (c) 2013 Dr. Rainer Hessmer and is covered by the permissive MIT license.</li>
		<li>Many thanks to Michal Zalewski's for his <a href=" http://lcamtuf.coredump.cx/gcnc/">Guerrilla guide to CNC machining, mold making, and resin casting</a>, in particular <a href=" http://lcamtuf.coredump.cx/gcnc/">section 6.2. Creating spur gears</a>(you need to scroll down a bt to get to section 6.2).</p>.</li>
        <li>This web page heavily leverages the <a href="http://joostn.github.io/OpenJsCad/">OpenJsCad</a> framework developed by <a href="https://github.com/joostn">Joost Nieuwenhuijse</a>. <a href="http://joostn.github.io/OpenJsCad/">OpenJsCad</a> in turn leverages the <a href="https://github.com/evanw/csg.js">initial CSG.js</a> copyright (c) 2011 <a href="https://github.com/evanw">Evan Wallace</a> and <a href="https://github.com/evanw/lightgl.js">lightgl.js</a> by <a href="https://github.com/evanw"> Evan Wallace</a> and <a href="https://github.com/toaarnio"> Tomi Aarnio</a> for WebGL rendering. Contributions by <a href="https://github.com/alx"> Alexandre Girard</a>, <a href="https://github.com/tlrobinson">Tom Robinson</a>, <a href="https://github.com/jboecker">jboecker</a>, <a href="https://github.com/risacher">risacher</a> and <a href="https://github.com/tedbeer">tedbeer</a>. All code released under the MIT license.</li>
    </ul>

    </body>
</html>
